# Макросы

[Встроенные методы][inline] предоставляют элегантную технику метапрограммирования, 
выполняя некоторые операции во время компиляции. 
Однако иногда встраивания недостаточно, и нужны более мощные способы анализа и синтеза программ во время компиляции. 
Макросы позволяют делать именно это: относиться к **программам как к данным** и манипулировать ими.

### Макросы рассматривают программы как значения

С помощью макроса программы можно рассматривать как значения, 
что позволяет анализировать и генерировать их во время компиляции. 
Выражение Scala с типом `T` представлено экземпляром типа `scala.quoted.Expr[T]`.

Более детально о типе `Expr[T]`, а также различные способы анализа и построения экземпляров, 
будут раскрыты в главах [Quoted Code][quoted-code] 
и [Reflection](../metaprogramming/reflection.md). 
Пока достаточно знать, что макросы — это метапрограммы, которые манипулируют выражениями типа `Expr[T]`.

Следующая реализация макроса просто печатает выражение предоставленного аргумента:

```dotty
def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] =
  println(x.show)
  x
```

После печати выражения аргумента исходный аргумент возвращается как выражение Scala типа `Expr[Any]`.

Как уже упоминалось в разделе [Inline][inline], 
встроенные методы предоставляют точку входа для определений макросов:

```dotty
inline def inspect(inline x: Any): Any = ${ inspectCode('x) }
```

Все макросы определены с расширением `inline def`. 
Реализация этой точки входа всегда имеет одинаковую форму:

- они содержат только одну [склейку][quoted-code] `${ ... }`
- склейка содержит единственный вызов метода, реализующего макрос (например `inspectCode`).
- вызов реализации макроса получает параметры в кавычках (то есть `'x` вместо `x`) и контекстное `Quotes`.

Вызов `inspect` макроса `inspect(sys error "abort")` выводит 
строковое представление выражения аргумента во время компиляции:

```dotty
scala.sys.error("abort")
```

#### Макросы и параметры типа

Если у макроса есть параметры типа, реализация также должна о них знать. 
Точно так же, как `scala.quoted.Expr[T]` представляет выражение Scala типа `T`, 
`scala.quoted.Type[T]` используется для представления типа Scala `T`.

```dotty
inline def logged[T](inline x: T): T = ${ loggedCode('x)  }

def loggedCode[T](x: Expr[T])(using Type[T], Quotes): Expr[T] = ...
```

И экземпляр `Type[T]`, и контекст `Quotes` автоматически предоставляются склейкой 
в соответствующем встроенном методе (то есть `logged`) и могут использоваться реализацией макроса.

#### Определение и использование макросов

Ключевое различие между встраиванием и макросами заключается в способе их оценки. 
Встраивание работает, переписывая код и выполняя оптимизацию на основе правил, известных компилятору. 
С другой стороны, макрос выполняет написанный пользователем код, генерирующий код, до которого макрос расширяется.

Технически компиляция встроенного кода `${ inspectCode('x) }` вызывает метод `inspectCode` 
во время компиляции (через Java reflection), а затем метод `inspectCode` выполняется как обычный код.

Чтобы иметь возможность выполнить `inspectCode`, нужно сначала скомпилировать его исходный код. 
Как следствие, мы не можем определить и использовать макрос в одном и том же классе/файле. 
Однако можно иметь определение макроса и его вызов в одном и том же проекте, 
если реализация макроса может быть скомпилирована первой.

> **Приостановленные файлы**
> 
> Чтобы разрешить определение и использование макросов в одном и том же проекте, 
> расширяются только те вызовы макросов, которые уже были скомпилированы. 
> Для всех остальных (неизвестных) вызовов макросов компиляция файла приостанавливается. 
> Приостановленные файлы компилируются только после успешной компиляции всех незаблокированных файлов. 
> В некоторых случаях будут циклические зависимости, которые будут блокировать завершение компиляции. 
> Чтобы получить больше информации о том, какие файлы приостановлены, 
> можно использовать флаг компилятора `-Xprint-suspension`.

#### Пример: статическая оценка `power` с помощью макросов

Вспомним определение `power` из раздела [Inline][inline], 
которое специализировало вычисление `xⁿ` для статически известных значений `n`.

```dotty
inline def power(x: Double, inline n: Int): Double =
  inline if n == 0 then 1.0
  else inline if n % 2 == 1 then x * power(x, n - 1)
  else power(x * x, n / 2)
```

В оставшейся части этого раздела будет определен макрос, 
который вычисляет `xⁿ` для статически известных значений `x` и `n`. 
Хотя это также возможно с помощью `inline`, 
реализация с помощью макросов проиллюстрирует несколько вещей.

```dotty
inline def power(inline x: Double, inline n: Int) =
  ${ powerCode('x, 'n)  }

def powerCode(
  x: Expr[Double],
  n: Expr[Int]
)(using Quotes): Expr[Double] = ...
```

### Простые выражения

`powerCode` можно было бы реализовать следующим образом:

```dotty
def pow(x: Double, n: Int): Double =
  if n == 0 then 1 else x * pow(x, n - 1)

def powerCode(
  x: Expr[Double],
  n: Expr[Int]
)(using Quotes): Expr[Double] =
  val value: Double = pow(x.valueOrError, n.valueOrError)
  Expr(value)
```

Здесь операция `pow` представляет собой простую функцию Scala, которая вычисляет значение `xⁿ`. 
Интересная часть заключается в том, как мы создаем и изучаем `Expr`-ы.

#### Создание выражений из значений

Давайте сначала посмотрим `Expr.apply(value)`. 
Учитывая значение типа `T`, этот вызов вернет выражение, содержащее код, 
представляющий данное значение (то есть типа `Expr[T]`). 
Значение аргумента `Expr` вычисляется во время компиляции, 
во время выполнения нужно только создать экземпляр этого значения.

Создание выражений из значений работает для всех примитивных типов, кортежей любой арности, 
`Class`, `Array`, `Seq`, `Set`, `List`, `Map`, `Option`, `Either`, `BigInt`, `BigDecimal`, `StringContext`. 
Другие типы также могут работать, если для них реализован `ToExpr`, 
[как будет показано позже][quoted-code].

#### Извлечение значений из выражений

Второй метод, который используется при реализации `powerCode` - это `Expr[T].valueOrError`, 
имеющий эффект, противоположный `Expr.apply`. 
Он пытается извлечь значение типа `T` из выражения типа `Expr[T]`. 
Это может быть успешным только в том случае, если выражение непосредственно содержит код значения, 
в противном случае будет выдано исключение, которое остановит раскрытие макроса и сообщит, 
что выражение не соответствует значению.

Вместо `valueOrError`, можно было бы также использовать операцию `value`, которая вернет `Option`. 
Таким образом, можно сообщить об ошибке с помощью пользовательского сообщения.

```dotty
  ...
  (x.value, n.value) match
    case (Some(base), Some(exponent)) =>
      pow(base, exponent)
    case (Some(_), _) =>
      report.error("Expected a known value for the exponent, but was " + n.show, n)
    case _ =>
      report.error("Expected a known value for the base, but was " + x.show, x)
```

Кроме того, также можно использовать экстрактор `Expr.unapply`:

```dotty
  ...
  (x, n) match
    case (Expr(base), Expr(exponent)) =>
      pow(base, exponent)
    case (Expr(_), _) => ...
    case _ => ...
```

Операции `value`, `valueOrError` и `Expr.unapply` будут работать для всех примитивных типов, кортежей любой арности, 
`Option`, `Seq`, `Set`, `Map`, `Either` и `StringContext`. 
Другие типы также могут работать, если для них реализован FromExpr, 
[как будет показано позже][quoted-code].

#### Отображение выражений

В реализации `inspectCode` было видно, как преобразовать выражения 
в строковое представление исходного кода с помощью метода `.show`. 
Это может быть полезно для отладки реализации макросов:

```dotty
def debugPowerCode(
  x: Expr[Double],
  n: Expr[Int]
)(using Quotes): Expr[Double] =
  println(
    s"""powerCode
       |  x := ${x.show}
       |  n := ${n.show}""".stripMargin)
  val code = powerCode(x, n)
  println(s"  code := ${code.show}")
  code
```

#### Работа с переменными аргументами

Переменные аргументы в Scala представлены с помощью `Seq`, 
поэтому, когда пишется макрос с переменным аргументом, он будет передан как `Expr[Seq[T]]`. 
Можно восстановить каждый отдельный аргумент (типа `Expr[T]`) с помощью экстрактора `scala.quoted.Varargs`.

```dotty
import scala.quoted.Varargs

inline def sumNow(inline nums: Int*): Int =
  ${ sumCode('nums)  }

def sumCode(nums: Expr[Seq[Int]])(using Quotes): Expr[Int] =
  nums match
    case  Varargs(numberExprs) => // numberExprs: Seq[Expr[Int]]
      val numbers: Seq[Int] = numberExprs.map(_.valueOrError)
      Expr(numbers.sum)
    case _ => report.error(
      "Expected explicit argument" +
      "Notation `args: _*` is not supported.", numbersExpr)
```

Экстрактор сопоставит вызов `sumNow(1, 2, 3)` и извлечет `Seq[Expr[Int]]`, содержащий код каждого параметра. 
Но если попытаться сопоставить аргумент вызова `sumNow(nums: _*)`, экстрактор не совпадет.

`Varargs` также может быть использован в качестве конструктора. 
`Varargs(Expr(1), Expr(2), Expr(3))` вернет `Expr[Seq[Int]]`.

### Сложные выражения

Было показано, как создавать и распаковывать выражения, соответствующие простым значениям. 
Для работы с более сложными выражениями Scala предлагает различные средства метапрограммирования, начиная от

- дополнительные конструкторы, такие как `Expr.apply`,
- [сопоставление с образцом в цитатах][quoted-code],
- [reflection API](../metaprogramming/reflection.md);

каждый из них усложняется и потенциально теряет гарантии безопасности. 
Обычно рекомендуется чаще использовать простые API. 
В оставшейся части этого раздела вводятся еще несколько дополнительных конструкторов и деструкторов, 
а в последующих главах представлены более продвинутые API.

#### Коллекции

Было показано, как преобразовать `List[Int]` в `Expr[List[Int]]` используя `Expr.apply`. 
Как насчет преобразования `List[Expr[Int]]` в `Expr[List[Int]]`? 
Упоминалось, что `Varargs.apply` может сделать это для последовательностей; 
аналогично, для других типов коллекций доступны соответствующие методы:

- `Expr.ofList`: преобразует `List[Expr[T]]` в `Expr[List[T]]`
- `Expr.ofSeq`: преобразует `Seq[Expr[T]]` в `Expr[Seq[T]]` (так же, как Varargs)
- `Expr.ofTupleFromSeq`: преобразует `Seq[Expr[T]]` в `Expr[Tuple]`
- `Expr.ofTuple`: преобразует `(Expr[T1], ..., Expr[Tn])` в `Expr[(T1, ..., Tn)]`

#### Простые блоки

Конструктор `Expr.block` предоставляет простой способ создания блока кода `{ stat1; ...; statn; expr }`. 
Его первые аргументы — это список со всеми операторами, а второй аргумент — выражение в конце блока.

```dotty
inline def test(inline ignore: Boolean, computation: => Unit): Boolean =
  ${ testCode('ignore, 'computation) }

def testCode(ignore: Expr[Boolean], computation: Expr[Unit])(using Quotes) =
  if ignore.valueOrError then Expr(false)
  else Expr.block(List(computation), Expr(true))
```

Конструктор `Expr.block` полезен, когда необходимо сгенерировать код, содержащий несколько побочных эффектов. 
Вызов макроса `test(false, EXPRESSION)` будет генерировать `{ EXPRESSION; true}`, 
в то время как вызов `test(true, EXPRESSION)` приведет к `false`.

#### Простое сопоставление

Этот метод `Expr.matches` можно использовать для проверки равенства одного выражения другому. 
С помощью этого метода можно было бы реализовать `value` операцию `Expr[Boolean]` следующим образом:

```dotty
def value(boolExpr: Expr[Boolean]): Option[Boolean] =
  if boolExpr.matches(Expr(true)) then Some(true)
  else if boolExpr.matches(Expr(false)) then Some(false)
  else None
```

Его также можно использовать для сравнения двух написанных пользователем выражений. 
Обратите внимание, что `matches` выполняется только ограниченная нормализация, 
и хотя, например, Scala выражение `2` соответствует выражению `{ 2 }`, 
это не относится к выражению `{ val x: Int = 2; x }`.

#### Произвольные выражения

Можно создать произвольный код Scala `Expr[T]`, заключив его [в цитаты][quoted-code]. 
Например, `'{ ${expr}; true }` сгенерирует `Expr[Int]` эквивалент `Expr.block(List(expr), Expr(true))`. 
В следующем разделе, посвященном [Quoted Code][quoted-code], цитаты представлены более подробно.


---

**Ссылки:**

- [Scala 3 Guide](https://docs.scala-lang.org/scala3/guides/macros/macros.html)
- [Scala 3 Reference](https://docs.scala-lang.org/scala3/reference/metaprogramming/macros.html)

[quoted-code]: ../metaprogramming/quoted-code.md
[inline]: ../metaprogramming/inline.md
